﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Security.Application;
using TidyNet;

namespace CommonTools
{
    internal interface SanitizeStrategy
    {
        string Execute(Group tagStart, Group tagEnd, string tag, Group attributes);
    }

    internal class RemoveTagStrategy : SanitizeStrategy
    {
        #region SanitizeStrategy Members
        public string Execute(Group tagStart, Group tagEnd, string tag, Group attributes)
        {
            return string.Empty; 
        }
        #endregion
    }

    internal class EncodeTagStrategy : SanitizeStrategy 
    {
        #region SanitizeStrategy Members
        public string Execute(Group tagStart, Group tagEnd, string tag, Group attributes)
        {
            StringBuilder builder = new StringBuilder ();
            builder.Append(tagStart.Success ? tagStart.Value : "<");
            builder.Append(tag);
            builder.Append(tagEnd.Success ? tagEnd.Value : ">");

            return AntiXss.HtmlEncode(builder.ToString());
        }
        #endregion
    }

    public class HtmlUtil
    {
        private static readonly Regex HtmlTagExpression = new Regex(@"
            (?'tag_start'</?)
            (?'tag'\w+)((\s+
            (?'attribute'(\w+)(\s*=\s*(?:"".*?""|'.*?'|[^'"">\s]+)))?)+\s*|\s*)
            (?'tag_end'/?>)", 
            RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase | RegexOptions.Compiled);

        private static readonly Regex HtmlAttributeExpression = new Regex(@"
            (?'attribute'\w+)
            (\s*=\s*)
            (""(?'value'.*?)""|'(?'value'.*?)')", 
            RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase | RegexOptions.Compiled);
        
        private static readonly Dictionary<string, List<string>> ValidHtmlTags = new Dictionary<string, List<string>>
        {
            {"p", new List<string>          {}},
            {"div", new List<string>        {}},
            {"span", new List<string>       {}},
            {"br", new List<string>         {}},
            {"hr", new List<string>         {}},
            {"label", new List<string>      {}},
            
            {"h1", new List<string>         {}},
            {"h2", new List<string>         {}},
            {"h3", new List<string>         {}},
            {"h4", new List<string>         {}},
            {"h5", new List<string>         {}},
            {"h6", new List<string>         {}},
            
            {"pre", new List<string>        {}},
            {"strong", new List<string>     {}},
            {"b", new List<string>          {}},
            {"em", new List<string>         {}},
            {"i", new List<string>          {}},
            {"u", new List<string>          {}},
            {"strike", new List<string>     {}},
            {"ol", new List<string>         {}},
            {"ul", new List<string>         {}},
            {"li", new List<string>         {}},
            {"blockquote", new List<string> {}},
            {"code", new List<string>       {}},
            
            {"a", new List<string>          { "href", "title", "target"}},
            {"img", new List<string>        { "height", "width", "alt", "title", "hspace", "vspace", "border", "src"}},

            {"table", new List<string>      {}},
            {"thead", new List<string>      {}},
            {"tbody", new List<string>      {}},
            {"tfoot", new List<string>      {}},
            {"th", new List<string>         {"scope"}},
            {"tr", new List<string>         {}},
            {"td", new List<string>         {"colspan"}},

            {"q", new List<string>          {"cite"}},
            {"cite", new List<string>       {}},
            {"abbr", new List<string>       {}},
            {"acronym", new List<string>    {}},
            {"del", new List<string>        {}},
            {"ins", new List<string>        {}}
        };
        
        /// <summary>
        /// Removes the invalid HTML tags.
        /// </summary>
        /// <param name="input">The text.</param>
        /// <returns></returns>
        public static string RemoveInvalidHtmlTags(string input)
        {
            var html = TidyHtml(input);

            if (string.IsNullOrEmpty(html))
                return AntiXss.HtmlEncode(input);

            return HtmlTagExpression.Replace(html, new MatchEvaluator(match =>
            {              
                var builder = new StringBuilder(match.Length);

                var tagStart = match.Groups["tag_start"];
                var tagEnd = match.Groups["tag_end"];
                var tag = match.Groups["tag"].Value;
                var attributes = match.Groups["attribute"];

                if (false == ValidHtmlTags.ContainsKey(tag))
                {
                    RemoveTagStrategy re = new RemoveTagStrategy();
                    return re.Execute (tagStart, tagEnd, tag, attributes); 
                }

                builder.Append(tagStart.Success ? tagStart.Value : "<");
                builder.Append(tag);

                foreach (Capture attribute in attributes.Captures)
                {
                    builder.Append(MatchHtmlAttribute(tag, attribute));
                }

                // add nofollow to all hyperlinks
                if (tagStart.Success && tagStart.Value == "<" && tag.Equals("a", StringComparison.OrdinalIgnoreCase))
                    builder.Append(" rel=\"nofollow\"");

                builder.Append(tagEnd.Success ? tagEnd.Value : ">");

                return builder.ToString();
            }));
        }

        private static string MatchHtmlAttribute(string tag, Capture capture)
        {
            var output = string.Empty;
            var match = HtmlAttributeExpression.Match(capture.Value);

            var attribute = match.Groups["attribute"].Value;
            var value = match.Groups["value"].Value;
            
            if (ValidHtmlTags[tag].Contains(attribute))
            {
                switch (attribute)
                {
                    case "src":
                    case "href":
                        if (Regex.IsMatch(value, @"https?://[^""]+"))
                            output = string.Format(" {0}=\"{1}\"", attribute, AntiXss.UrlEncode(value));
                        break;
                    default:
                        output = string.Format(" {0}=\"{1}\"", attribute, value);
                        break;
                }
            }

            return output;
        }

        private static string TidyHtml(string text)
        {
            var doc = new Tidy();
            var messages = new TidyMessageCollection();
            var input = new MemoryStream();
            var output = new MemoryStream();

            var array = Encoding.UTF8.GetBytes(text);
            input.Write(array, 0, array.Length);
            input.Position = 0;

            doc.Options.DocType = DocType.Strict;
            doc.Options.Xhtml = true;
            doc.Options.LogicalEmphasis = true;
            doc.Options.DropFontTags = true;
            doc.Options.DropEmptyParas = true;
            doc.Options.QuoteAmpersand = true;
            doc.Options.TidyMark = false;
            doc.Options.MakeClean = true;
            doc.Options.IndentContent = false;
            doc.Options.SmartIndent = false;
            doc.Options.Spaces = 3;
            doc.Options.WrapLen = 120;
            doc.Options.CharEncoding = CharEncoding.UTF8;

            
            doc.Parse(input, output, messages);

            return RemoveTidyAdditions( Encoding.UTF8.GetString(output.ToArray()) );
        }

        private static string RemoveTidyAdditions(string text)
        {
            if (string.IsNullOrEmpty(text))
                return string.Empty;

            Regex bodyExpression = new Regex(@"<body[^>]+>");
            if (bodyExpression.IsMatch(text))
            {
                Match match = bodyExpression.Match(text);
                var start = match.Index;
                var end = text.IndexOf("</body>");

                if (start != -1 && end > start && end < text.Length)
                    text = text.Substring(start + match.Length, end - (start + match.Length));
                else
                    return string.Empty;
            }

            return Regex.Replace(text, "[\r\n\t]*", string.Empty);
        }
    }
}
